\documentclass[12pt]{report}
\usepackage{geometry}
\geometry{a4paper,scale=0.8}
\usepackage{longtable}
\usepackage{url}
\usepackage{listings}
\usepackage{graphicx}
\usepackage{float}
\usepackage{titlesec}
\usepackage{CJK}
\begin{CJK}{UTF8}{gkai}
\title{A000 PC上电和系统运行过程初体验}
\author{Chuzy}
\date{2020/05/03}

\lstset{
    basicstyle          =   \small\ttfamily,    % 基本代码风格
    keywordstyle        =   \bfseries,          % 关键字风格
    commentstyle        =   \rmfamily\itshape,  % 注释的风格，斜体
    stringstyle         =   \ttfamily,          % 字符串风格
    flexiblecolumns,                            % 别问为什么，加上这个
    numbers             =   left,               % 行号的位置在左边
    showspaces          =   false,  % 是否显示空格，显示了有点乱，所以不现实了
    numberstyle         =   \ttfamily,    % 行号的样式，小五号，tt等宽字体
    showstringspaces    =   false,
    captionpos          =   t,      % 这段代码的名字所呈现的位置，t指的是top上面
    frame               =   lrtb,   % 显示边框
    columns         =   flexible,     
    breaklines      =   true,
    language        =  [ANSI]C,
    escapeinside=``
}


%以上部分叫做"导言区",下面才开始写正文
\begin{document}

%先插入标题
\maketitle
\tableofcontents
\newpage 


\chapter{概述}
本文借用了同济大学赵炯博士重新编写的Linux0.00代码，包含3个文件：boot.s, head.s 和Makefile， 代码通过两个进程切换，在屏幕分别输出‘AAA...’和‘BBB...’。
\begin{figure}[htbp]
\centering
\includegraphics[scale=0.5]{fig/31.png}
\caption{linux0.00在bochs仿真运行}
\label{fig:linux0.00 bochs simulation}
\end{figure}
\chapter{流程图}
\begin{figure}[htbp]
\centering
\includegraphics[scale=0.5]{fig/38.png}
\caption{A000流程图}
\label{fig:A000 Flowchart}
\end{figure}

\chapter{引导扇区源码 boot.s}
PC上电后，CPU进入实地址模式，从ROM-BIOS地址0xFFFF0（1M-15）处开始执行，ROM-BIOS会先执行系统自检，在物理内存0处构造中断向量表，然后将可启动设备的磁盘引导扇区（512字节）读入绝对地址0x7C00（31K）内存处并跳转到这里开始执行代码。\\\\
boot.s即被写入引导扇区的512字节的源代码，这部分代码编译后在实模式下运行，使用Intel格式汇编语言编写，通过as86和ld86将boot.s编译。

\section{从磁盘读入系统映像} 
ROM-BIOS读入可启动设备的磁盘引导扇区（512字节）到0x7C00（31K）内存处并跳转到这里开始执行。相当于我们的代码此时开始接管系统的控制权。第一件事是将系统映像完整的读入到SYSSEG指向的内存。ROM-BIOS已经读入了一个扇区，我们从第二个扇区开始读取，加载到SYSSEG = 0x1000 (64K)处。

\begin{lstlisting}
BOOTSEG = 0x07c0	! 0x7c00, 80386 left-shift 4bits. `段地址自动偏移`
SYSSEG  = 0x1000	! system loaded at 0x10000 (65536). `固定将系统加载到64K处`
SYSLEN  = 17		! sectors occupied.  `假设我们的系统17个扇区足够了`

entry start
start:
	jmpi	go,#BOOTSEG  ! `jmpi会将CS：EIP自动赋值, cs被设置为0x7c00`
go:	mov	ax,cs
	mov	ds,ax
	mov	ss,ax
	mov	sp,#0x400	! arbitrary value >>512 
				! `BIOS只加载了一个扇区0x200到0x7c00`
				! `ss=0x7c00, sp=0x400到sp=0x200之间为安全的堆栈空间`

load_system:
	mov	dx,#0x0000
	mov	cx,#0x0002 ! Sector No. start from 1. `从第二个扇区开始读取`
	mov	ax,#SYSSEG
	mov	es,ax
	xor	bx,bx
	mov	ax,#0x200+SYSLEN
	int 	0x13 ! `通过BIOS中断读磁盘`.
	jnc	ok_load
die:	jmp	die
\end{lstlisting}
\section{搬移系统到0地址}
刚刚读入的内核代码，复制到以0为开始的内存空间中(es=0)。0x2000=8192个word，16k字节，实际上大于我们刚刚读入的17个扇区。由于ROM-BIOS初始在0地址构造了中断向量表，我们的这个搬移操作后，就不能再使用ROM-BIOS中断了。
\begin{lstlisting}
ok_load:
	cli		! no interrupts allowed !
	mov	ax, #SYSSEG
	mov	ds, ax
	xor	ax, ax
	mov	es, ax
	mov	cx, #0x2000
	sub	si,si
	sub	di,di
	rep
	movw
\end{lstlisting}	
\section{进入保护模式}
加载idt和gdt表, 为进入保护模式做准备，idt和gdt表的内容在进入保护模式后还会重写，在boot.s中先有一个最简单的定义。
\begin{lstlisting}
	mov	ax, #BOOTSEG
	mov	ds, ax
	lidt	idt_48 ! load idt with 0,0
	lgdt	gdt_48 ! load gdt with whatever appropriate
	
	mov	ax,#0x0001 ! protected mode (PE) bit
	lmsw	ax		! This is it!
\end{lstlisting}
\section{保护模式下的跳转}
开启保护模式了后，jmpi就要按照保护模式的寻址方式进行跳转。8是保护模式下的段选择符，0是偏移地址；（段选择符bit[15-3]为段索引，bit2为0表示GDT，1表示LDT，bit[1:0]表示RPL。因此8 = 0b0001000, 说明是GDT的索引为1的段(GDT中索引为0的段保留),特权等级为0.
\begin{lstlisting}
	jmpi	0,8 ! jmp offset 0 of segment 8 (cs)
\end{lstlisting}
\section{GDT/IDT}
GDT/IDT表定义，这里IDT表为空。
\begin{lstlisting}
gdt:	.word	0,0,0,0 ! dummy `索引为0的段, 保留`
		 ! `索引为1`
	.word	0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)
	.word	0x0000 ! base address=0x00000
	.word	0x9A00 ! code read/exec `代码段`
	.word	0x00C0 ! granularity=4096, 386
1		 ! `索引为2`
	.word	0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)
	.word	0x0000 ! base address=0x00000
	.word	0x9200 ! data read/write `数据段`
	.word	0x00C0 ! granularity=4096, 386

idt_48: .word	0 ! idt limit=0
	.word	0,0 ! idt base=0L
gdt_48: .word	0x7ff ! gdt limit=2048, 256 GDT entries
	.word	0x7c00+gdt,0 ! gdt base = 07xxx	
\end{lstlisting}
\section{引导扇区填充}
填充满一个扇区，引导扇区必须以0xAA55作为结尾标识。
\begin{lstlisting}
.org 510
	.word   0xAA55
\end{lstlisting}

\chapter{保护模式代码 head.s}
由上节看到，在boot.s中，从第二个扇区开始的系统代码被读入到内存0地址处。在boot.s最后，通过jmpi将控制权交给加载在地址0处的代码段，这里就是head.s编译后被加载到的位置。\\\\
head.s包含32位保护模式初始化设置代码、时钟中断代码、系统调用中断代码和两个任务的代码, 在时钟中断控制下进行任务0和1之间的切换操作，分别在屏幕上输出'A'或者'B'。\\\\
head.s采用AT\&T 的格式编写，通过as编译生成32为代码。
\section{定义代码入口}
在使用LD程序，链接多个目标模块时，LD会根据它们的类别把各个目标模块中的相应段分别组合在一起，LD需要一个入口标志，在链接时可以为设置代码入口点。startup\_32 即我们的入口标志，在Makefile中我们将此标志提供给LD，并指定startup\_32 将被加载到0地址处。
\begin{lstlisting}
#`段选择符定义`
#`段选择符bit[15-3]为段索引，bit2为0表示GDT，1表示LDT，bit[1:0]表示RPL`
SCRN_SEL	= 0x18 #[0...0011 0 00], 3rd in GDT
TSS0_SEL	= 0x20 #[0...0100 0 00], 4th in GDT
LDT0_SEL	= 0x28 #[0...0101 0 00], 5th in GDT
TSS1_SEL	= 0X30 #[0...0110 0 00], 6th in GDT
LDT1_SEL	= 0x38 #[0...0111 0 00], 7th in GDT

.global startup_32
.text
startup_32:  
\end{lstlisting}
\section{重新设置IDT/GDT}
首先使用boot.s中定义的GDT表，初始化数据段和堆栈段，根据GDT的定义，各段的线性基地址都是0。然后调用子程序重新设置IDT和GDT表，并重置段地址寄存器。
\begin{lstlisting}   
	movl $0x10,%eax #[0...0010 0 00], 2nd in GDT
	mov %ax,%ds
	lss init_stack,%esp #`LSS装入全指针指令 操作数分别为32位偏移量和段选择符。`
	
	call setup_idt
	call setup_gdt
	movl $0x10,%eax
	mov %ax,%ds    
	mov %ax,%es
	mov %ax,%fs
	mov %ax,%gs
	lss init_stack,%esp	#`重置堆栈，使用的实际还是相同堆栈。`
\end{lstlisting}

\section{设置定时器}
head.s中通过定时器产生中断，控制在两个task之间的跳转。定时器中断由intel8253芯片产生。可编程计数器/定时器有几种芯片型号，外形引脚及功能都是兼容的，只是工作的最高计数速率有所差异，例如8253（2.6MHz），8253-5(5MHz)。\\\\
8253内部有三个完全独立的计数器，分别称为计数器0、计数器1和计数器2。每个计数器通过三个引脚和外部联系，一个为时钟输入端CLK，一个为门控信号输入端GATE，另一个为输出端OUT。\\\\
PC分配给8253三个计数器的端口地址分别为40H~42H，控制字寄存器的端口址为43H。这里我们使用计数器0,设置其读写16位、方式3、2进制计数器值，并设置其频率为100Hz，即10ms定时器。

\begin{lstlisting}   
	movb $0x36, %al #00110110 `0x36 控制字：通道0，读写16位、方式3、2进制计数器值`
	movl $0x43, %edx	#0x43 `控制端口`
	outb %al, %dx
	movl $11930, %eax # timer frequency 100 HZ 
	movl $0x40, %edx	#0x40 `计数器0`
	outb %al, %dx
	movb %ah, %al
	outb %al, %dx
\end{lstlisting}
\section{设置中断向量}
在实地址模式中，CPU把内存中从0开始的1K字节作为一个中断向量表。表中的每个表项占四个字节，由两个字节的段地址和两个字节的偏移量组成，这样构成的地址便是相应中断处理程序的入口地址。在保护模式下，中断向量表中的表项由8个字节组成，中断向量表也改叫做中断描述符表IDT（InterruptDescriptor Table）。IDT中的每个表项叫做一个门描述符（gate descriptor），“门”的含义是当中断发生时必须先通过这些门，然后才能进入相应的处理程序。门描述符(Gate Descriptor)有三种类型：中断门，陷阱门，任务门。\\\\
head.s需要使用两个中断门描述符：时钟中断timer\_interrupt 和系统调用中断system\_interrupt ；时钟中断timer\_interrupt 由定时器产生；而系统调用中断system\_interrupt 由一条AT\&T 语法的中断指令int \$0x80产生。\\\\
先构造定时器中断处理的门描述符。首先是在EAX中构造低32bit, 0x00080000 中高位的0x0008（0b1000）对应段选择符，1为索引, 0表示GDT, RPL = 00 表示最高优先级; 低位的0x0000被置为timer\_interrupt 的地址。即中断响应函数段选择符为8, 偏移为timer\_interrupt。\\\\
接着在EDX中构造门描述符的高32bit，由于timer\_interrupt 偏移的高位部分均为0，只需要处理高32bit中的低16bit, 即0x8e00，其中0xe=0b1110，表示描述符为中断描述符； 0x8 = 0b1000 ,即P=1，表示段存在标记；DPL=00，表示访问限制在内核态。\\\\
在实模式中，定时中断的向量号为8，这里我们也把第8个中断门描述符设置为保护定时器中断处理的门描述符. 一个异常或中断向量号只占用1字节，所以一个IDT最多可以有256项, 第0项是可以被使用的，其中空描述符的Pr标记位需要被设置为0。这里先通过lea获取到idt表中索引为8的表项的地址，idt+0+ecx*8；再将EAX的内容写入该地址，将EDX的内容写入高四字节。

\begin{lstlisting}   
# setup timer & system call interrupt descriptors.
	movl $0x00080000, %eax	
	movw $timer_interrupt, %ax
	movw $0x8E00, %dx

	lea idt(,%ecx,8), %esi
	movl %eax,(%esi) 
	movl %edx,4(%esi)
\end{lstlisting}
类似的，按常规我们将第0x80(128)个描述符项作设置为系统调用。低32bit中，system\_interrupt 和timer\_interrupt 处理函数的段地址相同，offset不同；高32bit中，描述符类型不同。 0xef00 中0xf = 0b1111，表示描述符为陷阱描述符；0xe = 0b1110, P=1，表示段存在标记；DPL=11，表示访问在用户态。
\begin{lstlisting}   
	movw $system_interrupt, %ax
	movw $0xef00, %dx
	movl $0x80, %ecx
	lea idt(,%ecx,8), %esi
	movl %eax,(%esi) 
	movl %edx,4(%esi)
\end{lstlisting}
\section{进入用户态，跳转到任务0}
由于处于特权级0的代码不能直接把控制权转移到特权级3的代码中执行，但中断返回操作是可以的，因此我们利用中断返回指令IRET来启动任务0(A)。我们需要先在堆栈中人工建立中断返回时的场景，这就需要用到EFLAGS寄存器中的NTbit了。\\\\
EFLAGS寄存器的NT(bit 14) [Nested task flag]，控制中断链和被调用任务。若当前任务与前一个执行任务相关则置1，反之则清零。在使用CALL指令、中断或异常执行任务调用时，处理器会设置该标志。在通过使用IRET指令从一个任务返回时，处理器会检查并修改这个NT标志。具体规定如下：(1) 当NT=0，用堆栈中保存的值恢复EFLAGS、CS和EIP，执行常规的中断返回操作；(2) 当NT=1，通过任务转换实现中断返回（使用TSS中保存的值）。这里我们要把NT置零。\\\\
把任务0的TSS段选择符加载到任务寄存器LTR中、LDT段选择符加载到LDTR中，把任务0的堆栈指针(0x17:init\_stack)和代码指针(0x0f:task0)以及标志寄存器压入栈中，然后执行中断返回指令IRET。\\\\
IRET指令会弹出堆栈上的堆栈指针作为任务0的用户栈指针，恢复任务0的标志寄存器内容，并且弹出栈中代码指针放入CS：EIP寄存器中，从而开始执行任务0的代码，完成从特权级0到特权级3的控制转移。这里的0x17是任务0的局部数据段选择符，0x17的二进制为[00010 1 11]故为段选择Index=2，TI=1(表示在LDT中)，RPL=3的段描述符，根据该段描述符可知为数据段。

\begin{lstlisting}   
# Move to user mode (task 0)
	pushfl
	andl $0xffffbfff, (%esp) #`NT=0`
	popfl
	
	movl $TSS0_SEL, %eax	
	ltr %ax
	movl $LDT0_SEL, %eax
	lldt %ax 
	movl $0, current #`保存进程号的全局变量`
	sti
	pushl $0x17
	pushl $init_stack
	pushfl
	pushl $0x0f #text segment selector in LDT
	pushl $task0
	iret
\end{lstlisting}
\section{setup\_gdt和setup\_idt子程序}
IDT表最大有256个门描述符，不象GDT预先设置好。setup\_idt子程序，首先在256项中都设置中断处理函数为ignore\_int,然后用lidt加载IDT表。
\begin{lstlisting} 
setup_gdt:
	lgdt lgdt_opcode
	ret
setup_idt:
	lea ignore_int,%edx
	movl $0x00080000,%eax
	movw %dx,%ax /* selector = 0x0008 = cs */
	movw $0x8E00,%dx/* interrupt gate - dpl=0, present */
	lea idt,%edi
	mov $256,%ecx
rp_sidt:
	movl %eax,(%edi)
	movl %edx,4(%edi)
	addl $8,%edi
	dec %ecx
	jne rp_sidt
	lidt lidt_opcode
	ret	
\end{lstlisting}
\section{write\_char子程序}
write\_char 显示字符子程序。取当前光标位置并把AL中的字符显示在屏幕上。变量scr\_loc 中保存目前字符显示位置值，屏幕上每个字符占2个字节，出位置外还有一个属性字节，通过左移1bit，可以得到显存中的位置。然后把al中保存的需要显示的字符，mov到显存对应的位置。然后除2加1，得到下一个字符的显示位置，当超过2000（80*25）时，位置复位为0。
\begin{lstlisting} 
write_char:
	push %gs
	pushl %ebx
#	pushl %eax
	mov $SCRN_SEL, %ebx
	mov %bx, %gs
	movl scr_loc, %ebx
	shl $1, %ebx
	movb %al, %gs:(%ebx)
	shr $1, %ebx
	incl %ebx
	cmpl $2000, %ebx
	jb 1f
	movl $0, %ebx
1:	movl %ebx, scr_loc	
#	popl %eax
	popl %ebx
	pop %gs
	ret
\end{lstlisting}
\section{ignore\_int子程序}
ignore\_int 是默认的中断处理程序，若系统产生了中断，则会载屏幕显示一个字符‘C’，这里的align，推测是该函数的入口地址，需要保存在IDT表中。
\begin{lstlisting} 
/* This is the default interrupt "handler" :-) */
.align 8
ignore_int:
	push %ds
	pushl %eax
	movl $0x10, %eax
	mov %ax, %ds /*ds seclector in GDT*/
	movl $67, %eax /* print 'C' */
	call write_char
	popl %eax
	pop %ds
	iret
\end{lstlisting}
\section{timer\_interrupt子程序}
定时器中断处理程序，我们依赖其和全局变量current，执行任务切换操作。
\begin{lstlisting} 
/* Timer interrupt handler */ 
.align 2
timer_interrupt:
	push %ds
	pushl %eax
	movl $0x10, %eax
	mov %ax, %ds /*ds seclector in GDT*/
	movb $0x20, %al /*EOI end of interrupt*/
	outb %al, $0x20 /*Other inerrupt allowed*/
	movl $1, %eax
	cmpl %eax, current
	je 1f
	movl %eax, current
	ljmp $TSS1_SEL, $0
	jmp 2f
1:	movl $0, current
	ljmp $TSS0_SEL, $0
2:	popl %eax
	pop %ds
	iret
\end{lstlisting}
\section{system\_interrupt子程序}
由系统调用中断int0x80调用，通过调用write\_char子程序显示一个字符。
\begin{lstlisting} 
/* system call handler */
.align 2
system_interrupt:
	push %ds
	pushl %edx
	pushl %ecx
	pushl %ebx
	pushl %eax
	movl $0x10, %edx
	mov %dx, %ds
	call write_char
	popl %eax
	popl %ebx
	popl %ecx
	popl %edx
	pop %ds
	iret
\end{lstlisting}
\section{变量和IDT/GDT定义}
IDT表按照最大长度预留空间，在设置中断向量的函数中设置了定时器中断和系统调用；GDT表中定义了8项，其中0x0 保留不使用；0x08 对应内核代码段；0x10 对应内核数据段；0x18 对应内核显存段；0x20 和0x28 对应任务0的tss 和ldt ；0x30 和0x38 对应任务1的tss 和ldt。
\begin{lstlisting} [breaklines]
current:.long 0 #`当前进程号`
scr_loc:.long 0 #`屏幕上的显示位置`

.align 8
lidt_opcode:
	.word 256*8-1 # idt contains 256 entries  `长度`
	.long idt # This will be rewrite by code. `起始地址`
lgdt_opcode:
	.word (end_gdt-gdt)-1 # so does gdt       `长度`
	.long gdt # This will be rewrite by code. `起始地址`

.align 64
idt:
	.fill 256,8,0 #uninitialized

gdt:
	.quad 0x0000000000000000 /* NULL descriptor `保留`*/
	.quad 0x00c09a00000007ff /* 0x7ff-8MB, base-0, 0xc09a-`32位内核不一致代码段`*/
	.quad 0x00c09200000007ff /* 0x7ff-8MB, base-0, 0xc092-`32位内核数据段`*/
	.quad 0x00c0920b80000002 /* 0x02-8KB, base-0xb8000, `32位内核数据段-显存`*/

	.word 0x0068, tss0, 0xe900, 0x0 # `tss0的段选择符,长度0x68,用户态可执行段`
	.word 0x0040, ldt0, 0xe200, 0x0 # `ldt0的段选择符,长度0x40,用户态数据段`
	.word 0x0068, tss1, 0xe900, 0x0 # `tss1的段选择符,长度0x68,用户态可执行段`
	.word 0x0040, ldt1, 0xe200, 0x0 # `ldt1的段选择符,长度0x40,用户态数据段`
end_gdt:
\end{lstlisting}

\section{堆栈空间}
先分配堆栈128*4字节的空间，然后设置堆栈指针，含一个4字节的offset（指向栈顶）和一个2字节的段选择符（0x10指向GDT表中的堆栈段）。
\begin{lstlisting} 
	.fill 128,4,0
init_stack: # Will be used as user stack for task0.
	.long init_stack
	.word 0x10
\end{lstlisting}
\section{任务0的LDT和TSS}
GDT中的LDT0描述符指向这里。
\begin{lstlisting} 
.align 64
ldt0:
	.quad 0x0000000000000000 # `保留`
	.quad 0x00c0fa00000003ff # 0x3ff-4MB, 0xc0fa-`32位用户态代码段`
	.quad 0x00c0f200000003ff # 0x3ff-4MB, 0xc0f2-`32位用户态数据段`
\end{lstlisting}
TSS(Task-State Segment)是为了解决调用门中特权级变换时堆栈的切换，每一个任务是最多可能在四个特权级间转移，所以每个任务实际上需要4个堆栈，当特权等级发生变化时，对应的堆栈要切换，被调用者（内层，高特权级）的堆栈的SS和ESP就是从这里取得的。GDT中的TSS0描述符指向这里，共26个long，104字节。从内核态到TSS0的第一次切换是通过iret进行的，堆栈信息是通过手工设置好的，不使用TSS结构。后续任务0和任务1之间的切换，就将使用这里的TSS0结构。
\begin{lstlisting} 
tss0:
	.long 0 /* back link */
	.long krn_stk0, 0x10 /* esp0, ss0 */
	.long 0, 0, 0, 0, 0 /* esp1, ss1, esp2, ss2, cr3 */
	.long 0, 0, 0, 0, 0 /* eip, eflags, eax, ecx, edx */
	.long 0, 0, 0, 0, 0 /* ebx esp, ebp, esi, edi */
	.long 0, 0, 0, 0, 0, 0 /* es, cs, ss, ds, fs, gs */
	.long LDT0_SEL, 0x8000000 /* ldt, trace bitmap */
\end{lstlisting}
\section{任务0的内核堆栈}
\begin{lstlisting} 
	.fill 128,4,0
krn_stk0:
\end{lstlisting}
任务0的用户栈是在IRET时，手工的把任务0的用户栈基址入栈的，我们用的其实就是init\_stack。
\section{任务1的LDT和TSS}
任务1的LDT和任务0基本相同。
\begin{lstlisting} 
.align 8
ldt1:
	.quad 0x0000000000000000
	.quad 0x00c0fa00000003ff # 0x0f
	.quad 0x00c0f200000003ff # 0x17
\end{lstlisting}
任务1不再是手工进入，这里的段选择符（倒数第2行）需要设置为正确的值。
\begin{lstlisting} 
tss1:
	.long 0 /* back link */
	.long krn_stk1, 0x10 /* esp0, ss0 */
	.long 0, 0, 0, 0, 0 /* esp1, ss1, esp2, ss2, cr3 */
	.long task1, 0x200 /* eip, eflags */
	.long 0, 0, 0, 0 /* eax, ecx, edx, ebx */
	.long usr_stk1, 0, 0, 0 /* esp, ebp, esi, edi */
	.long 0x17,0x0f,0x17,0x17,0x17,0x17
	.long LDT1_SEL, 0x8000000 /* ldt, trace bitmap */
\end{lstlisting}
\section{任务1的内核堆栈}
\begin{lstlisting} 
	.fill 128,4,0
krn_stk1:
\end{lstlisting}
\section{任务0和任务1代码}
\begin{lstlisting} 
task0:
	movl $0x17, %eax
	movw %ax, %ds
	movb $65, %al /* print 'A' */
	int $0x80 /*call sytem interrupt*/
	movl $0xfff, %ecx
1:	loop 1b
	jmp task0 

task1:
	movl $0x17, %eax
	movw %ax, %ds
	movb $66, %al /* print 'B' */
	int $0x80
	movl $0xfff, %ecx
1:	loop 1b
	jmp task1
\end{lstlisting}
\section{任务1的用户态堆栈}
\begin{lstlisting} 
	.fill 128,4,0 
usr_stk1:
\end{lstlisting}

\chapter{工程文件 Makefile}
\section{编译工具}
boot.s使用的是Intel格式的汇编语言编写，编译器需要使用as86，链接器使用ld86；
\begin{lstlisting} 
apt-cache search as86 ld86
bin86 - 16-bit x86 assembler and loader
\end{lstlisting}
说明bin86包含了我们所需要的两个软件.
\begin{lstlisting} 
sudo apt-get install bin86
\end{lstlisting}
\section{Makefile}
由于要在64位环境下汇编生成32汇编的代码，需要对as增加参数, 修改后的Makefile：
\begin{lstlisting}
# Makefile for the simple example kernel.
AS86	=as86 -0 -a
LD86	=ld86 -0
AS	=as --32
LD	=ld
LDFLAGS =-m elf_i386 -Ttext 0 -e startup_32 -s -x -M  

all:	Image

Image: boot system
	dd bs=32 if=boot of=Image skip=1
	objcopy -O binary system head
	cat head >> Image

disk: Image
	dd bs=8192 if=Image of=/dev/fd0
	sync;sync;sync

head.o: head.s

system:	head.o 
	$(LD) $(LDFLAGS) head.o  -o system > System.map

boot:	boot.s
	$(AS86) -o boot.o boot.s
	$(LD86) -s -o boot boot.o

clean:
	rm -f Image System.map core boot head *.o system
\end{lstlisting}
\chapter{模拟运行}
\section{在Bochs中模拟运行}
\paragraph{1} 安装bochs模拟器
\begin{lstlisting}
sudo apt install bochs
sudo apt-get install bochs-x
\end{lstlisting}
\paragraph{2} 修改bochs配置文件
运行bochs，选择“3. Edit options”，进入子菜单，选择“11. Disk \& Boot options”，设置从软盘A加载，加载文件名为Image；然后逐层退回主菜单，选择“4. Save options to...”，我们这里给的文件名是"fd.bxrc"，关键部分为：
\begin{lstlisting} 
...
boot: floppy
floppy_bootsig_check: disabled=0
floppya: type=1_44, 1_44="Image", status=inserted, write_protected=0
...
\end{lstlisting}
\paragraph{3} 仿真运行，
在命令行执行：
\begin{lstlisting} 
bochs -f fd.bxrc
\end{lstlisting}
进入bochs以后黑屏是进入了debug模式, 在终端输入c回车即可

\end{CJK}
\end{document}
